package de.dnb.basics.utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import org.marc4j.marc.DataField;
import org.marc4j.marc.Record;
import org.marc4j.marc.Subfield;

import de.dnb.basics.applicationComponents.RangeCheckUtils;
import de.dnb.basics.applicationComponents.StringUtils;
import de.dnb.basics.filtering.FilterUtils;
import de.dnb.basics.filtering.IFunction;

public final class DDCMarcUtils {

    /**
     * Nummern haben mindestens 3 Ziffern.
     */
    private static final int MINIMAL_DDC_NUMBER_LENGTH = 3;

    private DDCMarcUtils() {
    }

    /**
     * 
     * @param record	nicht null
     * @return			true, wenn 765-Feld (synth. Nummer) vorhanden
     */
    public static boolean isSynthesizedNumber(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 12) == 'b';
        //        return MarcUtils.containsField(record, "765");
    }

    /**
     * 
     * Wird die Nummer in einer Haupt- oder Hilfstafel angezeigt?
     * Kriterium ist, dass das letzte Zeichen (Position 13) im
     * Feld 008 ein 'a' ist (andernfalls == 'b').
     * 
     * @param record    nicht null
     * @return          Ob die Nummer im Druckwerk auftaucht
     */
    public static boolean isDisplayedInStandardSchedulesOrTables(
        final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 13) == 'a';
    }

    /**
     * 
     * Gelöschte Notation.
     * 
     * @param record    nicht null
     * @return          Ob gelöscht
     */
    public static boolean isDeleted(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 13) == 'h';
    }

    /**
     * 
     * Optionale Notation. Beispiel: (780.16).
     * 
     * @param record    nicht null
     * @return          Ob optional
     */
    public static boolean isOptional(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 9) == 'b';
    }

    /**
     * 
     * Standard, nicht optionale Notation.
     * 
     * @param record    nicht null
     * @return          Ob Standard
     */
    public static boolean isStandard(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 9) == 'a';
    }

    /**
     * 
     * Established.
     * <br>
     * Extent to which the formulation of the the 1XX number or term 
     * conforms to the classification scheme coded in field 084 
     * (Classification Scheme and Edition). 
     * 
     * @param record    nicht null
     * @return          Ob Standard
     */
    public static boolean isEstablished(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 11) == 'a';
    }

    /**
     * 
     * Obsolete Notation. Beispiel: [T1—0863]. Verlegt oder stillgelegt.
     * 
     * @param record    nicht null
     * @return          Ob Obsolet
     */
    public static boolean isObsolete(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 8) == 'e';
    }

    /**
     * 
     * Valide Notation.
     * 
     * @param record    nicht null
     * @return          Ob valide
     */
    public static boolean isValid(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 8) == 'a';
    }

    /**
     * Datensatz deckt einen Bereich (z.B. 004-006) ab.
     * 
     * @param record	nicht null
     * @return			kein Bereich
     */
    public static boolean isSpan(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 7) != 'a';
    }

    /**
     * Hilfstafel.
     * 
     * @param record    nicht null
     * @return          Hilfstafel
     */
    public static boolean isTable(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 6) == 'b';
    }

    /**
     * Haupttafel.
     * 
     * @param record    nicht null
     * @return          Haupttafel
     */
    public static boolean isSchedule(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String fixed = MarcUtils.getFixedLengthDataElements(record);
        return StringUtils.charAt(fixed, 6) == 'a';
    }

    /**
     * Ist Überblicks-Datensatz (Am Anfang eines Abschnittes)?
     * 
     * @param record	nicht null
     * @return			true, wenn Nummer weniger als 2 Ziffern hat (false
     * 					sonst, auch wenn kein 153-Feld vorhanden)			
     */
    public static boolean isOverview(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        String number = getClassificationNumber(record);
        if (number == null)
            return false;
        return number.length() < MINIMAL_DDC_NUMBER_LENGTH;
    }

    /**
     * Ist der Datensatz eine Anhängeanweisung (erkennbar an 2 $a)?
     * 
     * @param record nicht null
     * @return		Ist der Datensatz eine Anhängeanweisung?
     */
    public static boolean isAddInstruction(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        DataField nameField = getNumberField(record);
        if (nameField == null)
            return false;
        List<Subfield> aSubfields = nameField.getSubfields('a');
        return aSubfields.size() == 2;
    }

    /**
     * Gibt die Felder, die Registereinträge enthalten.
     * 
     * @param record	nicht null
     * @return			Indexfelder (700, 710, 711, 720, 730, 748, 750, 751, 
     * 					753, 754)
     */
    public static List<DataField> getIndexTermFields(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> variableFields =
            MarcUtils.getDataFields(record, "700", "710", "711", "720", "730",
                "748", "750", "751", "753", "754");
        return variableFields;
    }

    /**
     * Gibt die Registereinträge (Einrückungen durch '--' gekennzeichnet).
     * 
     * @param record	nicht null
     * @return			Liste der Registereinträge eines DDC-Marc-Datensatzes
     */
    public static List<String> getIndexTerms(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> indexFields = getIndexTermFields(record);
        List<String> indexTerms = new ArrayList<>();
        for (DataField dataField : indexFields) {
            String s = dataField.getSubfield('a').getData();
            List<Subfield> subsx = dataField.getSubfields('x');
            for (Subfield subfield : subsx) {
                s += "--" + subfield.getData();
            }
            indexTerms.add(s);
        }
        return indexTerms;
    }

    /**
     * Gibt den Namen der DDC-NUmmer.
     * 
     * @param record	nicht null
     * @return			Namen oder null, wenn nicht vorhanden
     */
    public static String getCaption(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        DataField dataField = getNumberField(record);
        Subfield captionSubfield = dataField.getSubfield('j');
        if (captionSubfield == null)
            return null;
        return captionSubfield.getData();
    }

    /**
     * Gibt das 153-Feld, das den Eintrag in der Tafel (Nummer + Name) enthält.
     * 
     * @param record	nicht null
     * @return			Feld 153 oder null, wenn nicht vorhanden
     */
    public static DataField getNumberField(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        DataField dataField = (DataField) record.getVariableField("153");
        return dataField;
    }

    /**
     * 
     * @param record	nicht null
     * @return			true, wenn 153-Feld enthalten
     */
    public static boolean isDDCRecord(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        return getNumberField(record) != null;
    }

    /**
     * Gibt die Klassifikationsnummer (mit 'TX--' vorneweg).
     * 
     * 
     * @param record	nicht null
     * @return			Nummer oder null
     */
    public static String getClassificationNumber(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        DataField numberField = getNumberField(record);
        if (numberField == null)
            return null;
        String s;
        // Tabellen-Nummer in $z:
        Subfield table = numberField.getSubfield('z');
        if (table != null)
            s = "T" + table.getData() + "--";
        else
            s = "";
        return s + numberField.getSubfield('a').getData();
    }

    /**
     * Gibt die Basisnummmer, an die angehängt wurde.
     * 
     * @param field765  nicht null, tag = 765
     * @return          Nummer oder null, wenn nicht vorhanden
     */
    public static String getBaseNumber(final DataField field765) {
        RangeCheckUtils.assertReferenceParamNotNull("field765", field765);
        String tag = field765.getTag();
        if (!tag.equals("765"))
            throw new IllegalArgumentException("Feld " + field765
                + "hat nicht tag 765");
        ArrayList<Subfield> subs = new ArrayList<>(field765.getSubfields());
        for (int i = 0; i < subs.size(); i++) {
            Subfield sub = subs.get(i);
            if (sub.getCode() == 'b') {
                // z zuvor? Dann Hilfstafel
                if (i > 0) {
                    Subfield subZ = subs.get(i - 1);
                    if (subZ.getCode() == 'z') {
                        String table = subZ.getData();
                        return "T" + table + "--" + sub.getData();
                    }
                }
                // keine Hilfstafel
                String raw = sub.getData();
                return StringUtils.rightPadding(raw, 3, '0');
            }
        }

        return null;
    }

    /**
     * DIE Basis-Nummer, an die angehängt wurde. Sie steht im letzten
     * 765-Feld. Wenn nicht angehängt wurde, einfach die DDC-Nummer.
     * 
     * @param record    nicht null
     * @return          Nummer oder null, wenn nicht vorhanden
     */
    public static String getBaseNumber(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> fields765 = MarcUtils.getDataFields(record, "765");
        int size = fields765.size();
        if (size == 0)
            return getClassificationNumber(record);
        DataField field765 = fields765.get(size - 1);
        return getBaseNumber(field765);
    }

    public static String getPicaFields(final Record record, int number) {
        String s = "";
        String synt = getClassificationNumber(record);
        if (!synt.startsWith("T"))
            s += "54" + number + "0 [DDC22ger]" + synt + "\n";
        String base = getBaseNumber(record);
        if (base.startsWith("T"))
            s += "54" + number + "3 -" + base + "\n";
        else
            s += "54" + number + "1 " + base + "\n";
        ArrayList<String> adds = getAddNumbers(record);
        //@formatter:off
        for (ListIterator<String> iterator 
                = adds.listIterator(adds.size()); 
                iterator.hasPrevious();) {
            String add = (String) iterator.previous();
            //@formatter:on
            if (add.startsWith("T"))
                s += "54" + number + "3 -" + add + "\n";
            else
                s += "54" + number + "2 " + add + "\n";
        }
        return s;
    }

    /**
     * Nummer, die angehängt wurde. Diese steht im Feld 765 in den
     * Unterfeldern $r (Root) und $s (angehängt). Steht vor diesen
     * direkt ein $z, so ist es eine Hilfstafelnotation.
     * 
     * @param field765  nicht null
     * @return          Nummer oder null, wenn nicht gefunden
     */
    public static String getAddNumber(final DataField field765) {
        RangeCheckUtils.assertReferenceParamNotNull("field765", field765);
        String tag = field765.getTag();
        if (!tag.equals("765"))
            throw new IllegalArgumentException("Feld " + field765
                + "hat nicht tag 765");

        ArrayList<Subfield> subs = new ArrayList<>(field765.getSubfields());
        for (int i = 0; i < subs.size(); i++) {
            Subfield sub = subs.get(i);
            if (sub.getCode() == 'r' || sub.getCode() == 's') {
                String dataZ = "";
                String dataSub = sub.getData();
                String dataNext = "";
                // z zuvor?
                if (i > 0) {
                    Subfield subZ = subs.get(i - 1);
                    if (subZ.getCode() == 'z') {
                        dataZ = "T" + subZ.getData() + "--";
                    }
                }
                if (i < subs.size() - 1) {
                    Subfield subNext = subs.get(i + 1);
                    if (subNext.getCode() == 'r' || subNext.getCode() == 's') {
                        dataNext = subNext.getData();
                    }
                }
                if (dataZ.equals(""))
                    return concatenate(dataSub, dataNext);
                else
                    return dataZ + dataSub + dataNext;

            }
        }
        return null;

    }

    /**
     * Gibt die Nummern, die an die Grundnotation angehängt werden,
     * um die synthetisierte Nummer zu erhalten. Es werden alle
     * 765-Felder ausgewertet.
     * 
     * 
     * @param record    nicht null, auf DDC wird nicht geprüft.
     * @return          nicht null, ohne null in der Collection
     */
    public static ArrayList<String> getAddNumbers(final Record record) {
        RangeCheckUtils.assertReferenceParamNotNull("record", record);
        List<DataField> fields765 = MarcUtils.getDataFields(record, "765");
        IFunction<DataField, String> function =
            new IFunction<DataField, String>() {
                @Override
                public String apply(final DataField x) {
                    return getAddNumber(x);
                }
            };
        return FilterUtils.mapNullFiltered(fields765, function);
    }

    /**
     * Baut die Nummer und fügt, wenn nötig, an dritter Stelle einen Punkt ein.
     * @param root      nicht null
     * @param added     nicht null
     * @return          DDC-Nummer
     */
    public static String concatenate(final String root, final String added) {
        StringBuffer buffer = new StringBuffer(root);
        buffer.append(added);
        int len = buffer.length();
        if (len < MINIMAL_DDC_NUMBER_LENGTH)
            throw new IllegalArgumentException("weniger als 3 Ziffern");
        if (len > MINIMAL_DDC_NUMBER_LENGTH) {
            if (!root.contains("."))
                buffer.insert(MINIMAL_DDC_NUMBER_LENGTH, '.');
        }
        return buffer.toString();
    }

    public static void main(final String[] args) {

        Record record = MarcUtils.readXMLfromClip();
        System.out.println(record);
        System.out.println(getBaseNumber(record));
    }

}
